/*
 * Copyright 2010 Srikanth Reddy Lingala
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.lingala.zip4j.util;

import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;

import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipModel;

public class Zip4jUtil {

    public static boolean isStringNotNullAndNotEmpty(String str) {
        if (str == null || str.trim().length() <= 0) {
            return false;
        }

        return true;
    }

    public static boolean checkOutputFolder(String path) throws ZipException {
        if (!isStringNotNullAndNotEmpty(path)) {
            throw new ZipException(new NullPointerException("output path is null"));
        }

        File file = new File(path);

        if (file.exists()) {

            if (!file.isDirectory()) {
                throw new ZipException("output folder is not valid");
            }

            if (!file.canWrite()) {
                throw new ZipException("no write access to output folder");
            }
        } else {
            try {
                file.mkdirs();
                if (!file.isDirectory()) {
                    throw new ZipException("output folder is not valid");
                }

                if (!file.canWrite()) {
                    throw new ZipException("no write access to destination folder");
                }

//				SecurityManager manager = new SecurityManager();
//				try {
//					manager.checkWrite(file.getAbsolutePath());
//				} catch (Exception e) {
//					e.printStackTrace();
//					throw new ZipException("no write access to destination folder");
//				}
            } catch (Exception e) {
                throw new ZipException("Cannot create destination folder");
            }
        }

        return true;
    }

    public static boolean checkFileReadAccess(String path) throws ZipException {
        if (!isStringNotNullAndNotEmpty(path)) {
            throw new ZipException("path is null");
        }

        if (!checkFileExists(path)) {
            throw new ZipException("file does not exist: " + path);
        }

        try {
            File file = new File(path);
            return file.canRead();
        } catch (Exception e) {
            throw new ZipException("cannot read zip file");
        }
    }

    public static boolean checkFileWriteAccess(String path) throws ZipException {
        if (!isStringNotNullAndNotEmpty(path)) {
            throw new ZipException("path is null");
        }

        if (!checkFileExists(path)) {
            throw new ZipException("file does not exist: " + path);
        }

        try {
            File file = new File(path);
            return file.canWrite();
        } catch (Exception e) {
            throw new ZipException("cannot read zip file");
        }
    }

    public static boolean checkFileExists(String path) throws ZipException {
        if (!isStringNotNullAndNotEmpty(path)) {
            throw new ZipException("path is null");
        }

        File file = new File(path);
        return checkFileExists(file);
    }

    public static boolean checkFileExists(File file) throws ZipException {
        if (file == null) {
            throw new ZipException("cannot check if file exists: input file is null");
        }
        return file.exists();
    }

    public static boolean isWindows() {
        String os = System.getProperty("os.name").toLowerCase();
        return (os.indexOf("win") >= 0);
    }

    public static void setFileReadOnly(File file) throws ZipException {
        if (file == null) {
            throw new ZipException("input file is null. cannot set read only file attribute");
        }

        if (file.exists()) {
            file.setReadOnly();
        }
    }

    public static void setFileHidden(File file) throws ZipException {
        if (file == null) {
            throw new ZipException("input file is null. cannot set hidden file attribute");
        }

        if (!isWindows()) {
            return;
        }

        if (file.exists()) {
            try {
                Runtime.getRuntime().exec("attrib +H \"" + file.getAbsolutePath() + "\"");
            } catch (IOException e) {
                // do nothing as this is not of a higher priority
                // add log statements here when logging is done
            }
        }
    }

    public static void setFileArchive(File file) throws ZipException {
        if (file == null) {
            throw new ZipException("input file is null. cannot set archive file attribute");
        }

        if (!isWindows()) {
            return;
        }

        if (file.exists()) {
            try {
                if (file.isDirectory()) {
                    Runtime.getRuntime().exec("attrib +A \"" + file.getAbsolutePath() + "\"");
                } else {
                    Runtime.getRuntime().exec("attrib +A \"" + file.getAbsolutePath() + "\"");
                }

            } catch (IOException e) {
                // do nothing as this is not of a higher priority
                // add log statements here when logging is done
            }
        }
    }

    public static void setFileSystemMode(File file) throws ZipException {
        if (file == null) {
            throw new ZipException("input file is null. cannot set archive file attribute");
        }

        if (!isWindows()) {
            return;
        }

        if (file.exists()) {
            try {
                Runtime.getRuntime().exec("attrib +S \"" + file.getAbsolutePath() + "\"");
            } catch (IOException e) {
                // do nothing as this is not of a higher priority
                // add log statements here when logging is done
            }
        }
    }

    public static long getLastModifiedFileTime(File file, TimeZone timeZone) throws ZipException {
        if (file == null) {
            throw new ZipException("input file is null, cannot read last modified file time");
        }

        if (!file.exists()) {
            throw new ZipException("input file does not exist, cannot read last modified file time");
        }

        return file.lastModified();
    }

    public static String getFileNameFromFilePath(File file) throws ZipException {
        if (file == null) {
            throw new ZipException("input file is null, cannot get file name");
        }

        if (file.isDirectory()) {
            return null;
        }

        return file.getName();
    }

    public static long getFileLengh(String file) throws ZipException {
        if (!isStringNotNullAndNotEmpty(file)) {
            throw new ZipException("invalid file name");
        }

        return getFileLengh(new File(file));
    }

    public static long getFileLengh(File file) throws ZipException {
        if (file == null) {
            throw new ZipException("input file is null, cannot calculate file length");
        }

        if (file.isDirectory()) {
            return -1;
        }

        return file.length();
    }

    /**
     * Converts input time from Java to DOS format
     *
     * @param time
     * @return time in DOS format
     */
    public static long javaToDosTime(long time) {

        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(time);

        int year = cal.get(Calendar.YEAR);
        if (year < 1980) {
            return (1 << 21) | (1 << 16);
        }
        return (year - 1980) << 25 | (cal.get(Calendar.MONTH) + 1) << 21 |
                cal.get(Calendar.DATE) << 16 | cal.get(Calendar.HOUR_OF_DAY) << 11 | cal.get(Calendar.MINUTE) << 5 |
                cal.get(Calendar.SECOND) >> 1;
    }

    /**
     * Converts time in dos format to Java format
     *
     * @param dosTime
     * @return time in java format
     */
    public static long dosToJavaTme(int dosTime) {
        int sec = 2 * (dosTime & 0x1f);
        int min = (dosTime >> 5) & 0x3f;
        int hrs = (dosTime >> 11) & 0x1f;
        int day = (dosTime >> 16) & 0x1f;
        int mon = ((dosTime >> 21) & 0xf) - 1;
        int year = ((dosTime >> 25) & 0x7f) + 1980;

        Calendar cal = Calendar.getInstance();
        cal.set(year, mon, day, hrs, min, sec);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime().getTime();
    }

    public static FileHeader getFileHeader(ZipModel zipModel, String fileName) throws ZipException {
        if (zipModel == null) {
            throw new ZipException("zip model is null, cannot determine file header for fileName: " + fileName);
        }

        if (!isStringNotNullAndNotEmpty(fileName)) {
            throw new ZipException("file name is null, cannot determine file header for fileName: " + fileName);
        }

        FileHeader fileHeader = null;
        fileHeader = getFileHeaderWithExactMatch(zipModel, fileName);

        if (fileHeader == null) {
            fileName = fileName.replaceAll("\\\\", "/");
            fileHeader = getFileHeaderWithExactMatch(zipModel, fileName);

            if (fileHeader == null) {
                fileName = fileName.replaceAll("/", "\\\\");
                fileHeader = getFileHeaderWithExactMatch(zipModel, fileName);
            }
        }

        return fileHeader;
    }

    public static FileHeader getFileHeaderWithExactMatch(ZipModel zipModel, String fileName) throws ZipException {
        if (zipModel == null) {
            throw new ZipException("zip model is null, cannot determine file header with exact match for fileName: " + fileName);
        }

        if (!isStringNotNullAndNotEmpty(fileName)) {
            throw new ZipException("file name is null, cannot determine file header with exact match for fileName: " + fileName);
        }

        if (zipModel.getCentralDirectory() == null) {
            throw new ZipException("central directory is null, cannot determine file header with exact match for fileName: " + fileName);
        }

        if (zipModel.getCentralDirectory().getFileHeaders() == null) {
            throw new ZipException("file Headers are null, cannot determine file header with exact match for fileName: " + fileName);
        }

        if (zipModel.getCentralDirectory().getFileHeaders().size() <= 0) {
            return null;
        }
        ArrayList fileHeaders = zipModel.getCentralDirectory().getFileHeaders();
        for (int i = 0; i < fileHeaders.size(); i++) {
            FileHeader fileHeader = (FileHeader) fileHeaders.get(i);
            String fileNameForHdr = fileHeader.getFileName();
            if (!isStringNotNullAndNotEmpty(fileNameForHdr)) {
                continue;
            }

            if (fileName.equalsIgnoreCase(fileNameForHdr)) {
                return fileHeader;
            }
        }

        return null;
    }

    public static int getIndexOfFileHeader(ZipModel zipModel,
                                           FileHeader fileHeader) throws ZipException {

        if (zipModel == null || fileHeader == null) {
            throw new ZipException("input parameters is null, cannot determine index of file header");
        }

        if (zipModel.getCentralDirectory() == null) {
            throw new ZipException("central directory is null, ccannot determine index of file header");
        }

        if (zipModel.getCentralDirectory().getFileHeaders() == null) {
            throw new ZipException("file Headers are null, cannot determine index of file header");
        }

        if (zipModel.getCentralDirectory().getFileHeaders().size() <= 0) {
            return -1;
        }
        String fileName = fileHeader.getFileName();

        if (!isStringNotNullAndNotEmpty(fileName)) {
            throw new ZipException("file name in file header is empty or null, cannot determine index of file header");
        }

        ArrayList fileHeaders = zipModel.getCentralDirectory().getFileHeaders();
        for (int i = 0; i < fileHeaders.size(); i++) {
            FileHeader fileHeaderTmp = (FileHeader) fileHeaders.get(i);
            String fileNameForHdr = fileHeaderTmp.getFileName();
            if (!isStringNotNullAndNotEmpty(fileNameForHdr)) {
                continue;
            }

            if (fileName.equalsIgnoreCase(fileNameForHdr)) {
                return i;
            }
        }
        return -1;
    }

    public static ArrayList getFilesInDirectoryRec(File path,
                                                   boolean readHiddenFiles) throws ZipException {

        if (path == null) {
            throw new ZipException("input path is null, cannot read files in the directory");
        }

        ArrayList result = new ArrayList();
        File[] filesAndDirs = path.listFiles();
        List filesDirs = Arrays.asList(filesAndDirs);

        if (!path.canRead()) {
            return result;
        }

        for (int i = 0; i < filesDirs.size(); i++) {
            File file = (File) filesDirs.get(i);
            if (file.isHidden() && !readHiddenFiles) {
                return result;
            }
            result.add(file);
            if (file.isDirectory()) {
                List deeperList = getFilesInDirectoryRec(file, readHiddenFiles);
                result.addAll(deeperList);
            }
        }
        return result;
    }

    public static String getZipFileNameWithoutExt(String zipFile) throws ZipException {
        if (!isStringNotNullAndNotEmpty(zipFile)) {
            throw new ZipException("zip file name is empty or null, cannot determine zip file name");
        }
        String tmpFileName = zipFile;
        if (zipFile.indexOf(System.getProperty("file.separator")) >= 0) {
            tmpFileName = zipFile.substring(zipFile.lastIndexOf(System.getProperty("file.separator")));
        }

        if (tmpFileName.indexOf(".") > 0) {
            tmpFileName = tmpFileName.substring(0, tmpFileName.lastIndexOf("."));
        }
        return tmpFileName;
    }

    public static byte[] convertCharset(String str) throws ZipException {
        try {
            byte[] converted = null;
            String charSet = detectCharSet(str);
            if (charSet.equals(InternalZipConstants.CHARSET_CP850)) {
                converted = str.getBytes(InternalZipConstants.CHARSET_CP850);
            } else if (charSet.equals(InternalZipConstants.CHARSET_UTF8)) {
                converted = str.getBytes(InternalZipConstants.CHARSET_UTF8);
            } else {
                converted = str.getBytes();
            }
            return converted;
        } catch (UnsupportedEncodingException err) {
            return str.getBytes();
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    /**
     * Decodes file name based on encoding. If file name is UTF 8 encoded
     * returns an UTF8 encoded string, else return Cp850 encoded String. If
     * appropriate charset is not supported, then returns a System default
     * charset encoded String
     *
     * @param data
     * @param isUTF8
     * @return String
     */
    public static String decodeFileName(byte[] data, boolean isUTF8) {
        if (isUTF8) {
            try {
                return new String(data, InternalZipConstants.CHARSET_UTF8);
            } catch (UnsupportedEncodingException e) {
                return new String(data);
            }
        } else {
            return getCp850EncodedString(data);
        }
    }

    /**
     * Returns a string in Cp850 encoding from the input bytes.
     * If this encoding is not supported, then String with the default encoding is returned.
     *
     * @param data
     * @return String
     */
    public static String getCp850EncodedString(byte[] data) {
        try {
            String retString = new String(data, InternalZipConstants.CHARSET_CP850);
            return retString;
        } catch (UnsupportedEncodingException e) {
            return new String(data);
        }
    }

    /**
     * Returns an absoulte path for the given file path
     *
     * @param filePath
     * @return String
     */
    public static String getAbsoluteFilePath(String filePath) throws ZipException {
        if (!isStringNotNullAndNotEmpty(filePath)) {
            throw new ZipException("filePath is null or empty, cannot get absolute file path");
        }

        File file = new File(filePath);
        return file.getAbsolutePath();
    }

    /**
     * Checks to see if all the elements in the arraylist match the given type
     *
     * @param sourceList - list to be checked
     * @param type       - type of elements to be present in the list (ex: File, String, etc)
     * @return true if all elements match the given type, if not returns false
     */
    public static boolean checkArrayListTypes(ArrayList sourceList, int type) throws ZipException {

        if (sourceList == null) {
            throw new ZipException("input arraylist is null, cannot check types");
        }

        if (sourceList.size() <= 0) {
            return true;
        }

        boolean invalidFound = false;

        switch (type) {
            case InternalZipConstants.LIST_TYPE_FILE:
                for (int i = 0; i < sourceList.size(); i++) {
                    if (!(sourceList.get(i) instanceof File)) {
                        invalidFound = true;
                        break;
                    }
                }
                break;
            case InternalZipConstants.LIST_TYPE_STRING:
                for (int i = 0; i < sourceList.size(); i++) {
                    if (!(sourceList.get(i) instanceof String)) {
                        invalidFound = true;
                        break;
                    }
                }
                break;
            default:
                break;
        }
        return !invalidFound;
    }

    /**
     * Detects the encoding charset for the input string
     *
     * @param str
     * @return String - charset for the String
     * @throws ZipException - if input string is null. In case of any other exception
     *                      this method returns default System charset
     */
    public static String detectCharSet(String str) throws ZipException {
        if (str == null) {
            throw new ZipException("input string is null, cannot detect charset");
        }

        try {
            byte[] byteString = str.getBytes(InternalZipConstants.CHARSET_CP850);
            String tempString = new String(byteString, InternalZipConstants.CHARSET_CP850);

            if (str.equals(tempString)) {
                return InternalZipConstants.CHARSET_CP850;
            }

            byteString = str.getBytes(InternalZipConstants.CHARSET_UTF8);
            tempString = new String(byteString, InternalZipConstants.CHARSET_UTF8);

            if (str.equals(tempString)) {
                return InternalZipConstants.CHARSET_UTF8;
            }

            return InternalZipConstants.CHARSET_DEFAULT;
        } catch (UnsupportedEncodingException e) {
            return InternalZipConstants.CHARSET_DEFAULT;
        } catch (Exception e) {
            return InternalZipConstants.CHARSET_DEFAULT;
        }
    }

    /**
     * returns the length of the string by wrapping it in a byte buffer with
     * the appropriate charset of the input string and returns the limit of the
     * byte buffer
     *
     * @param str
     * @return length of the string
     * @throws ZipException
     */
    public static int getEncodedStringLength(String str) throws ZipException {
        if (!isStringNotNullAndNotEmpty(str)) {
            throw new ZipException("input string is null, cannot calculate encoded String length");
        }

        String charset = detectCharSet(str);
        return getEncodedStringLength(str, charset);
    }

    /**
     * returns the length of the string in the input encoding
     *
     * @param str
     * @param charset
     * @return int
     * @throws ZipException
     */
    public static int getEncodedStringLength(String str, String charset) throws ZipException {
        if (!isStringNotNullAndNotEmpty(str)) {
            throw new ZipException("input string is null, cannot calculate encoded String length");
        }

        if (!isStringNotNullAndNotEmpty(charset)) {
            throw new ZipException("encoding is not defined, cannot calculate string length");
        }

        ByteBuffer byteBuffer = null;

        try {
            if (charset.equals(InternalZipConstants.CHARSET_CP850)) {
                byteBuffer = ByteBuffer.wrap(str.getBytes(InternalZipConstants.CHARSET_CP850));
            } else if (charset.equals(InternalZipConstants.CHARSET_UTF8)) {
                byteBuffer = ByteBuffer.wrap(str.getBytes(InternalZipConstants.CHARSET_UTF8));
            } else {
                byteBuffer = ByteBuffer.wrap(str.getBytes(charset));
            }
        } catch (UnsupportedEncodingException e) {
            byteBuffer = ByteBuffer.wrap(str.getBytes());
        } catch (Exception e) {
            throw new ZipException(e);
        }

        return byteBuffer.limit();
    }

    /**
     * Checks if the input charset is supported
     *
     * @param charset
     * @return boolean
     * @throws ZipException
     */
    public static boolean isSupportedCharset(String charset) throws ZipException {
        if (!isStringNotNullAndNotEmpty(charset)) {
            throw new ZipException("charset is null or empty, cannot check if it is supported");
        }

        try {
            new String("a".getBytes(), charset);
            return true;
        } catch (UnsupportedEncodingException e) {
            return false;
        } catch (Exception e) {
            throw new ZipException(e);
        }
    }

    public static ArrayList getSplitZipFiles(ZipModel zipModel) throws ZipException {
        if (zipModel == null) {
            throw new ZipException("cannot get split zip files: zipmodel is null");
        }

        if (zipModel.getEndCentralDirRecord() == null) {
            return null;
        }

        ArrayList retList = new ArrayList();
        String currZipFile = zipModel.getZipFile();
        String zipFileName = (new File(currZipFile)).getName();
        String partFile = null;

        if (!isStringNotNullAndNotEmpty(currZipFile)) {
            throw new ZipException("cannot get split zip files: zipfile is null");
        }

        if (!zipModel.isSplitArchive()) {
            retList.add(currZipFile);
            return retList;
        }

        int numberOfThisDisk = zipModel.getEndCentralDirRecord().getNoOfThisDisk();

        if (numberOfThisDisk == 0) {
            retList.add(currZipFile);
            return retList;
        } else {
            for (int i = 0; i <= numberOfThisDisk; i++) {
                if (i == numberOfThisDisk) {
                    retList.add(zipModel.getZipFile());
                } else {
                    String fileExt = ".z0";
                    if (i > 9) {
                        fileExt = ".z";
                    }
                    partFile = (zipFileName.indexOf(".") >= 0) ? currZipFile.substring(0, currZipFile.lastIndexOf(".")) : currZipFile + fileExt + (i + 1);
                    retList.add(partFile);
                }
            }
        }
        return retList;
    }

    public static String getRelativeFileName(String file, String rootFolderInZip, String rootFolderPath) throws ZipException {
        if (!Zip4jUtil.isStringNotNullAndNotEmpty(file)) {
            throw new ZipException("input file path/name is empty, cannot calculate relative file name");
        }

        String fileName = null;

        if (Zip4jUtil.isStringNotNullAndNotEmpty(rootFolderPath)) {

            File rootFolderFile = new File(rootFolderPath);

            String rootFolderFileRef = rootFolderFile.getPath();

            if (!rootFolderFileRef.endsWith(InternalZipConstants.FILE_SEPARATOR)) {
                rootFolderFileRef += InternalZipConstants.FILE_SEPARATOR;
            }

            String tmpFileName = file.substring(rootFolderFileRef.length());
            if (tmpFileName.startsWith(System.getProperty("file.separator"))) {
                tmpFileName = tmpFileName.substring(1);
            }

            File tmpFile = new File(file);

            if (tmpFile.isDirectory()) {
                tmpFileName = tmpFileName.replaceAll("\\\\", "/");
                tmpFileName += InternalZipConstants.ZIP_FILE_SEPARATOR;
            } else {
                String bkFileName = tmpFileName.substring(0, tmpFileName.lastIndexOf(tmpFile.getName()));
                bkFileName = bkFileName.replaceAll("\\\\", "/");
                tmpFileName = bkFileName + tmpFile.getName();
            }

            fileName = tmpFileName;
        } else {
            File relFile = new File(file);
            if (relFile.isDirectory()) {
                fileName = relFile.getName() + InternalZipConstants.ZIP_FILE_SEPARATOR;
            } else {
                fileName = Zip4jUtil.getFileNameFromFilePath(new File(file));
            }
        }

        if (Zip4jUtil.isStringNotNullAndNotEmpty(rootFolderInZip)) {
            fileName = rootFolderInZip + fileName;
        }

        if (!Zip4jUtil.isStringNotNullAndNotEmpty(fileName)) {
            throw new ZipException("Error determining file name");
        }

        return fileName;
    }

    public static long[] getAllHeaderSignatures() {
        long[] allSigs = new long[11];

        allSigs[0] = InternalZipConstants.LOCSIG;
        allSigs[1] = InternalZipConstants.EXTSIG;
        allSigs[2] = InternalZipConstants.CENSIG;
        allSigs[3] = InternalZipConstants.ENDSIG;
        allSigs[4] = InternalZipConstants.DIGSIG;
        allSigs[5] = InternalZipConstants.ARCEXTDATREC;
        allSigs[6] = InternalZipConstants.SPLITSIG;
        allSigs[7] = InternalZipConstants.ZIP64ENDCENDIRLOC;
        allSigs[8] = InternalZipConstants.ZIP64ENDCENDIRREC;
        allSigs[9] = InternalZipConstants.EXTRAFIELDZIP64LENGTH;
        allSigs[10] = InternalZipConstants.AESSIG;

        return allSigs;
    }
}
